今天的文章要來學習 LangChain 的 Models、Tools 和 Agents 的基本概念,並結合 PokéAPI 來打造一個可以分析寶可夢屬性剋制並推薦組隊建議的 Agent。
LangChain 的 Models 整合了各家模型供應商,提供了統一的介面,好處是換 Providers 時程式不用跟著一起改,可以透過同一個 init_chat_model 來初始化不同家的模型,但 init_chat_model 只是一個入口,真正跟各家 API 溝通的是各自獨立的 integration 套件,要另外安裝,舉例來說你在 init_chat_model 中調用了 OpenAI 的模型,背後實際用到的還是 langchain_openai,所以兩種寫法其實都可以用。
from langchain.chat_models import init_chat_model
model = init_chat_model(
"openai:gpt-6-astra",
api_key=...,
base_url=...,
)
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="gpt-6-astra",
api_key=...,
base_url=...,
)
LangChain 的 Tools 讓我們可以很方便地用 @tool 把普通的 Python function 註冊成 Agent 能使用的 Tool。@tool 會把 Function name 當成 Tool Name,利用 type hints 產生 input schema,再把 docstring 當成 Tool 的說明,讓模型知道這個 Tool 是做什麼的、什麼時候該用。
回到開頭主題,為了讓我的 LangChain Agent 成為寶可夢大師,我規劃了 4 個工具:
在此之前先寫一個所有 Tools 共用的 API 端點。
import requests
POKEAPI_BASE_URL = "https://pokeapi.co/api/v2"
def pokeapi_get(endpoint: str) -> dict:
"""呼叫 PokéAPI,統一處理 HTTP error。"""
try:
response = requests.get(
f"{POKEAPI_BASE_URL}/{endpoint}",
timeout=10,
)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
return {
"error": f"PokéAPI request failed: {str(e)}"
}
取得寶可夢中文名稱:
def get_species_name(species: dict) -> dict:
data = pokeapi_get(f"pokemon-species/{species['name']}")
chinese_name = None
for name_info in data["names"]:
if name_info["language"]["name"] == "zh-Hant":
chinese_name = name_info["name"]
break
return {
"id": data["id"],
"en": species["name"],
"zh": chinese_name,
}
多線程取得第一世代中英文清單:
from langchain.tools import tool
from functools import lru_cache
from concurrent.futures import ThreadPoolExecutor, as_completed
@tool
@lru_cache
def get_all_pokemon_names() -> list[dict]:
"""
取得第一世代所有 Pokémon 的中英文名稱對照表。
當需要將中文 Pokémon 名稱轉成 PokéAPI 使用的英文名稱,
或需要查看第一世代有哪些 Pokémon 時使用。
"""
data = pokeapi_get("generation/1")
species_list = data["pokemon_species"]
pokemon_list = []
with ThreadPoolExecutor(max_workers=8) as executor:
futures = [
executor.submit(
get_species_name,
species,
)
for species in species_list
]
for future in as_completed(futures):
pokemon_list.append(
future.result()
)
pokemon_list.sort(key=lambda x: x["id"])
return pokemon_list
@tool
@lru_cache
def get_pokemon(name: str) -> dict:
"""
取得指定 Pokémon 的基本資料。
適合用來查詢 Pokémon 的屬性與能力值。
name 必須使用英文名稱,例如 pikachu、charizard。
"""
data = pokeapi_get(f"pokemon/{name.lower()}")
if "error" in data:
return data
return {
"name": data["name"],
"types": [
item["type"]["name"]
for item in data["types"]
],
"stats": {
item["stat"]["name"]: item["base_stat"]
for item in data["stats"]
},
}
@tool
@lru_cache
def get_type_info(type_name: str) -> dict:
"""
取得 Pokémon 屬性的剋制與弱點。
type_name 必須使用英文,
例如 water、fire、electric、grass。
"""
data = pokeapi_get(f"type/{type_name.lower()}")
if "error" in data:
return data
relations = data["damage_relations"]
return {
"type": data["name"],
"double_damage_to": [
item["name"]
for item in relations["double_damage_to"]
],
"double_damage_from": [
item["name"]
for item in relations["double_damage_from"]
],
"half_damage_to": [
item["name"]
for item in relations["half_damage_to"]
],
"half_damage_from": [
item["name"]
for item in relations["half_damage_from"]
],
"no_damage_to": [
item["name"]
for item in relations["no_damage_to"]
],
"no_damage_from": [
item["name"]
for item in relations["no_damage_from"]
],
}
@tool
@lru_cache
def get_pokemon_by_type(
type_name: str,
limit: int = 10,
) -> dict:
"""
取得指定屬性的 Pokémon 候選名單。
當需要根據某個屬性挑選、推薦或組隊 Pokémon 時,
必須先使用此 Tool 取得候選名單,
不要直接依賴模型記憶提出 Pokémon 候選。
type_name 必須使用英文,例如 electric、grass。
limit 控制最多回傳多少隻 Pokémon。
"""
data = pokeapi_get(f"type/{type_name.lower()}")
if "error" in data:
return data
limit = max(1, min(limit, 30))
pokemon = [
item["pokemon"]["name"]
for item in data["pokemon"][:limit]
]
return {
"type": data["name"],
"count": len(data["pokemon"]),
"returned": len(pokemon),
"pokemon": pokemon,
}
根據 LangChain 對 Agents 的說明,Agent 可以理解成 Model + Harness。Harness 是什麼就不多廢話,可以看李弘毅教授的影片。反正 create_agent 就是在 Model 外面包上一套可設定的 Harness,包含 Tools、Prompt、State、Memory、Middleware 等能力;底層核心則是一個 ReAct-style 的循環:
Model → Tool → Model → Tool → … → Finish
總之今天仍然先做一個最基本的 agent,也就是 model + tools,再加 system_prompt。
from langchain.agents import create_agent
agent = create_agent(
model=model,
tools=[
get_all_pokemon_names,
get_pokemon,
get_type_info,
get_pokemon_by_type,
],
system_prompt="""
你是一個 Pokémon 組隊助手。
你的工作是根據使用者的需求,
查詢 PokéAPI 並提供 Pokémon 組隊建議。
規則:
1. 不要依賴自己的記憶猜測 Pokémon 的屬性、能力值或剋制關係,請透過 Tool 查資料再回答。
2. 使用 get_all_pokemon_names 取得第一世代寶可夢中英文資訊。
3. 使用 get_pokemon 先查對手的實際屬性。
3. 使用 get_type_info 查詢該屬性的弱點。
4. 使用 get_pokemon_by_type 取得候選 Pokémon。
5. 只能從 Tool 回傳的候選名單中挑選。
6. 再用 get_pokemon 驗證最後候選的詳細資料。
7. 最後使用繁體中文回答。
8. 清楚說明你推薦 Pokémon 的理由,以及使用到的屬性剋制關係。
""",
)
執行看看,這道館真的是夠隨機的了:
question="""
我要挑戰一個自創道館,道館會派出:
道館會派出以下 Pokémon:
- 鐵甲貝
- 椰蛋樹
- 三合一磁怪
請先查詢這些 Pokémon 的屬性與弱點,
再幫我從第一世代 Pokémon 中,依據屬性剋制關係,推薦 3 隻適合出戰的 Pokémon。
"""
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": question,
}
]
}
)
Tool Calls 歷程:
Tool Calls:
→ get_all_pokemon_names({})
Tool Calls:
→ get_pokemon({'name': 'cloyster'})
→ get_pokemon({'name': 'exeggutor'})
→ get_pokemon({'name': 'magneton'})
Tool Calls:
→ get_type_info({'type_name': 'water'})
→ get_type_info({'type_name': 'ice'})
→ get_type_info({'type_name': 'grass'})
→ get_type_info({'type_name': 'psychic'})
→ get_type_info({'type_name': 'electric'})
→ get_type_info({'type_name': 'steel'})
Tool Calls:
→ get_pokemon_by_type({'type_name': 'electric', 'limit': 20})
→ get_pokemon_by_type({'type_name': 'bug', 'limit': 20})
→ get_pokemon_by_type({'type_name': 'ground', 'limit': 20})
Tool Calls:
→ get_pokemon({'name': 'jolteon'})
→ get_pokemon({'name': 'pinsir'})
→ get_pokemon({'name': 'dugtrio'})
→ get_type_info({'type_name': 'bug'})
→ get_type_info({'type_name': 'ground'})
最終回答:
推薦隊伍:**雷伊布+凱羅斯+三地鼠**,分別主攻鐵甲貝、椰蛋樹與三合一磁怪。
以下已查詢對手屬性、屬性相剋、候選名單,並再次驗證三隻推薦寶可夢的資料。**採用 PokéAPI 現行屬性規則,隊員限定第一世代寶可夢**,不是初代遊戲的舊制規則;因此三合一磁怪按「電/鋼」計算。
## 一、道館寶可夢的屬性與弱點
雙屬性的受傷倍率需要相乘,不能只看其中一個屬性。
| 對手 | 查得屬性 | 受到 4 倍傷害 | 受到 2 倍傷害 |
|---|---|---|---|
| 鐵甲貝 | 水/冰 | 無 | 電、草、格鬥、岩石 |
| 椰蛋樹 | 草/超能力 | **蟲** | 火、冰、毒、飛行、幽靈、惡 |
| 三合一磁怪 | 電/鋼 | **地面** | 火、格鬥 |
**幾個重要細節:**
- **鐵甲貝不弱火、也不弱鋼**:冰的弱點會被水的抗性抵銷,兩者皆為 `2 × ½=1 倍`。
- **椰蛋樹四倍弱蟲**:草與超能力都弱蟲,合計 `2 × 2=4 倍`。
- **三合一磁怪四倍弱地面**:電與鋼都弱地面,合計 `2 × 2=4 倍`。
## 二、推薦出戰的 3 隻寶可夢
### 1. 雷伊布(Jolteon)——主攻鐵甲貝
**屬性:電|特攻種族值:110|速度種族值:130**
- 電屬性招式打水/冰的鐵甲貝為 **2 倍傷害**。
- 鐵甲貝的**防禦高達 180,但特防只有 45**,因此讓雷伊布使用**電屬性特殊招式**,比從物理面強攻更合理。
- 雷伊布速度種族值 130,高於鐵甲貝的 70,在等級與培育條件相近時有先手優勢。
**使用重點:**主要負責鐵甲貝;不要以電招硬打三合一磁怪,對方的電屬性使其只受到 **½ 倍**電傷害。
### 2. 凱羅斯(Pinsir)——主攻椰蛋樹
**屬性:蟲|攻擊種族值:125|速度種族值:85**
- 蟲屬性招式對椰蛋樹造成 **4 倍傷害**,是其最明顯的屬性破口。
- 凱羅斯攻擊種族值 125,適合使用**蟲屬性物理招式**。
- 速度種族值 85,高於椰蛋樹的 55,在相近條件下較有機會先攻。
- 純蟲屬性受到草招僅 **½ 倍傷害**,但受到超能力招式仍是 **1 倍**,不能把它當成全面抵抗椰蛋樹。
**使用重點:**務必配置所玩版本中可合法取得的蟲屬性攻擊招式;**自身是蟲屬性,不代表所有招式都能打出四倍傷害**。
### 3. 三地鼠(Dugtrio)——主攻三合一磁怪
**屬性:地面|攻擊種族值:100|速度種族值:120**
- 地面屬性招式對三合一磁怪造成 **4 倍傷害**。
- 地面屬性**免疫電屬性招式**,能在預判電招時提供換入機會。
- 速度種族值 120,高於三合一磁怪的 70;配合物理地面招式,適合快速進攻。
**使用重點:**三地鼠 HP 種族值只有 35、防禦 50,不宜隨意承受攻擊。它也弱水、冰與草,**避免拿它硬接鐵甲貝或椰蛋樹**。
## 三、建議對位
| 我方 | 主要對手 | 進攻方向 | 屬性倍率 |
|---|---|---|---|
| **雷伊布** | 鐵甲貝 | 電屬性特殊招式,攻擊低特防 | **2 倍** |
| **凱羅斯** | 椰蛋樹 | 蟲屬性物理招式 | **4 倍** |
| **三地鼠** | 三合一磁怪 | 地面屬性物理招式 | **4 倍** |
這套隊伍的優點是**分工明確,三隻都有對應的有利對位**。上述倍率僅指屬性相剋,尚未包含同屬性招式加成;實戰仍須考慮等級、招式、特性與道具,不能保證先手或一擊擊倒。
不過目前這個 Agent 只有考慮屬性關係而已,沒有考慮種族值或技能,而且只認識第一世代,距離成為真正的寶可夢大師還非常遙遠啊。
野生的 Tool 出現了,但 Agent 決定要不要理它。